// Dieses Script muss nach jQuery eingebunden werden
(function() {
  // to declare classes that get hooked into the global scope later

  /**
   * Class that represents a LastFm-user
   */
  function LastFmUser(name, realName, url, picUrl) {
    if (this === window) {
      return new LastFmUser('JD', 'John Doe', 'http://www.last.fm/user/rj', 'http://userserve-ak.last.fm/serve/252/76003280.jpg');
    }
    
    this.setName = function(userName) {
      name = userName;
    }
    
    this.setRealName = function(userRealName) {
      realName = userRealName;
    }
    
    this.setUrl = function(userUrl) {
      url = userUrl;
    }
    
    this.setPicUrl = function(userPicUrl) {
      picUrl = userPicUrl;
    }
    
    this.getName = function() {
      return name + '';
    }
    
    this.getRealName = function() {
      return realName + '';
    }
    
    this.getUrl = function() {
      return url;
    }
    
    this.getPicUrl = function() {
      return picUrl + '';
    }
  };
  
  
  /**
   * Class for a LastFm-event
   */
  function LastFmEvent(id, title, locName, city, country, lat, lon, lastfmUrl, website, image, date) {
    this.setId = function(eventId) {
      id = eventId;
    };
    this.setTitle = function(eventTitle) {
      title = eventTitle;
    };
    this.setLocName = function(eventLocName) {
      locName = eventLocName;
    };
    this.setCity = function(eventCity) {
      city = eventCity;
    };
    this.setCountry = function(eventCountry) {
      country = eventCountry;
    };
    this.setLat = function(eventLat) {
      lat = eventLat;
    };
    this.setLon = function(eventLon) {
      lon = eventLon;
    };
    this.setLastfmUrl = function(eventLastfmUrl) {
      lastfmUrl = eventLastfmUrl;
    };
    this.setWebsite = function(eventWebsite) {
      website = eventWebsite;
    };
    this.setImage = function(eventImage) {
      image = eventImage;
    };
    this.setDate = function(eventDate) {
      date = eventDate;
    };
    
    this.getId = function() {
      return id;
    };
    this.getTitle = function() {
      return title + '';
    };
    this.getLocName = function() {
      return locName + '';
    };
    this.getCity = function() {
      return city + '';
    };
    this.getCountry = function() {
      return country + '';
    };
    this.getLat = function() {
      return lat;
    };
    this.getLon = function() {
      return lon;
    };
    this.getLastfmUrl = function() {
      return lastfmUrl + '';
    };
    this.getWebsite = function() {
      return website + '';
    };
    this.getImage = function() {
      return image + '';
    };
    this.getDate = function() {
      return date;
    };
  };
  
  
  /**
   * This class holds users that are similar to the current user.
   * To each user there is a corresponding integer value
   * that specifies, on how many same past events the user has
   * been to.
   */
  function SimilarUsers(ignoreUser) {
    var that = this, users = [], createSimilarUser = function(name, common) {
      return { name: name, common: 0 };
    };
    
    ignoreUser = typeof ignoreUser === 'string' ? ignoreUser.toLowerCase() : null;
    
    this.getUsers = function() {
      return users;
    };
    
    /**
     * Takes a username as parameter and retrieves how many past
     * events current user and this user have in common, or -1 if
     * they do not have any in common.
     *
     * @return int, the number of common events or -1, if none
     */
    this.getCommonByUserName = function(username) {
      var index = that.getIndexByUserName(username);
      return index !== -1 ? users[index].common : -1;
    };
    
    /**
     * This adds the specified user to the user list with common
     * value 1, or if the user is already in the list, the common
     * value is raised by 1.
     *
     * @param String username the name to add
     */
    this.addUser = function(username) {
      var newUser, index = that.getIndexByUserName(username);
      
      if (username.toLowerCase() === ignoreUser) {
        return;
      }
      
      if(index === -1) {
        newUser = createSimilarUser(username);
        users.push(newUser);
      } else {
        users[index].common += 1;
      }
    };
    
    /**
     * This sorts the list of users by the number common
     * attendences. Operates on the users-array.
     */
    this.sortList = function() {
      users.sort(function(a, b) {
        return a.common === b.common ? 0 : (a.common < b.common ? 1 : -1);
      });
    };

    /**
     * This is a helper to find the index in the array for a
     * user with this username. Return -1 if user is not found,
     * or index if user is found.
     *
     * @param String username the name of the user you want the index for
     * @return int the index in the array of users or -1 if not found
     */
    this.getIndexByUserName = function(username) {
      var row;
      
      for (row = 0; row < users.length; ++row) {
        if (users[row].name === username) {
          return row;
        }
      }
      
      return -1;
    };
  };
  
  
  /**
   * This class holds events that could/should be recommendet to the
   * current user according to the similar past events. Each event
   * has a weight that is determined by how similar users are that
   * are singed up for the event to the current user.
   */
  function SuggestedEvents() {
    var that = this, events = [], createComparableEvent = function(theEvent, weight) {
      return { theEvent: theEvent, weight: weight, absWeight: 0 };
    };
    
    this.getEvents = function() {
      return events;
    };
    
    
    /**
     * Takes a event id as parameter and retrieves the weight, 
     * or -1 if this event was not found.
     *
     * @param String eventId the LastFm-event ID
     * @return int the index of the event in the array of events
     *  or -1 if not found
     */
    this.getWeightByEventId = function(eventId) {
      var index = that.getIndexByEventId(eventId);
      return index !== -1 ? events[index] : -1;
    };
    
    
    /**
     * This adds an event to the future event list with a specified
     * weight, or if the event is already in the list, the event's
     * weight is raised by the specified value.
     *
     * @param LastFmEvent theEvent the event you want to add
     * @param int weight the event's weight, > 0
     */
    this.addEvent = function(theEvent, weight) {
      var newEvent, index = that.getIndexByEventId(theEvent.getId());
      if(index === -1) {
        events.push(createComparableEvent(theEvent, weight));
      } else {
        events[index].weight += weight;
      }
    };
    
    
    /**
     * This sorts the list of events by weight. Operates on the list.
     */
    this.sortList = function() {
      events.sort(function(a, b) {
        return a.weight === b.weight ? 0 : (a.weight < b.weight ? 1 : -1);
      });
    };
    

    /**
     * This is a helper to find the index in the array for a
     * event with this event id. Return -1 if event is not found,
     * or index if event is found.
     *
     * @param String eventId the LastFmEvent's ID
     * @return int the index of the event in the array or -1 if not found
     */
    this.getIndexByEventId = function(eventId) {
      var row;
      
      for (row = 0; row < events.length; ++row) {
        if (events[row].theEvent.getId() === eventId) {
          return row;
        }
      }
      
      return -1;
    };
    
    
    /**
     * This function creates the final ranking for the future events. It
     * does not modify this object, but creates a new one and returns it.
     */
    this.createRanking = function(userLat, userLon, distanceWeight) {
      var newRankedSuggestions = new SuggestedEvents(), row, rankingValue, distance;

      for (row = 0; row < events.length; ++row) {
        distance = SuggestedEvents.calculateDistance(
          userLat, userLon, events[row].theEvent.getLat(), events[row].theEvent.getLon());
        rankingValue = that.calculateRankingValue(
          distance, events[row].weight, distanceWeight);

        newRankedSuggestions.addEvent(events[row].theEvent, rankingValue);
      }
      
      newRankedSuggestions.sortList();
  
      return newRankedSuggestions;
    };
    
    
    /**
     * Calculates the final ranking value of an event according to the
     * distance of the event to the user, the weight that was given to
     * the event earlier on, and the weight that users wish to put on
     * distance (between 0 and 1).
     */
    this.calculateRankingValue = function(distance, similarity, distanceWeight) {
      // TODO optimize that
      return ((similarity - 1) * (1 - distanceWeight) + 1) / ((distance - 1) * distanceWeight + 1);
    };
    
    
    /**
     * This function filters the list of suggested events by user
     * specified parameters and returns the result as new object
     *
     * @param minDate The lower date boundary as UNIX-timestamp (secs only)
     * @param maxDate Upper date boundary as UNIX-timestamp (secs only)
     * @param userLat Latitude of the user
     * @param userLon Longitude of the user
     * @param maxDistance Maximum distance in kilometers
     */
    this.filterEvents = function(minDate, maxDate, userLat, userLon, maxDistance) {
      var result = new SuggestedEvents(), row, distance, evt;

      for (row = 0; row < events.length; ++row) {
        evt = events[row].theEvent;
        if (evt.getDate() > minDate && evt.getDate() < maxDate
            && SuggestedEvents.calculateDistance(userLat, userLon, evt.getLat(), evt.getLon()) < maxDistance) {
          result.addEvent(evt, events[row].weight);
        }
      }
  
      return result;
    };
    
    
    /**
     * Should be called on this set, after filterEvents has been applied.
     * Sets the weight-property of each event to an absolute value 0 < v < 1.
     * Works on the set itself.
     *
     * @return SuggestedEvents this for chaining
     */
    this.calculateAbsoluteRanking = function() {
      var e, i, highest;
      
      for (i = 0; i < events.length; i++) {
        e = events[i];
        highest = i === 0 ? e.weight : highest;
        
        e.absWeight = e.weight / highest;
      }
      
      return this;
    };
  };
  /**
   * Calculates the distance between two geolocation points.
   * 
   * @see http://www.movable-type.co.uk/scripts/latlong.html
   * @since 1.1 now public static
   * @param lat1, lon1, lat2, lon2 float the latitudes and
   *  longitudes of point 1 and 2
   * @return the distance between those points in km
   */
  SuggestedEvents.calculateDistance = function(lat1, lon1, lat2, lon2) {
    var R = 6371, // km
      toRad = function(val) { return val * Math.PI / 180; },
      dLat = toRad(lat2-lat1), dLon = toRad(lon2-lon1),
      a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(toRad(lat1))
        * Math.cos(toRad(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2),
      c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
      
    return R * c;
  };
  
  
  // Expose classes to window
  window = window || {};
  window.LastFmUser = LastFmUser;
  window.LastFmEvent = LastFmEvent;
  window.SimilarUsers = SimilarUsers;
  window.SuggestedEvents = SuggestedEvents;
  
  
  // Little helper for Token, User & Events etc.
  var twoME107 = {
    token: null,
      // should be initialized upon DOM-ready
    
    lastResult: null,
      // shall contain the last ranked events, to apply a filter to them later
    
    // Object mit Konstanten, freeze() finalisiert das Objekt (wenn möglich).
    // hier können API-Url's etc. hin; alles was konstant ist
    // Object with constants, freeze() finalizes it if possible
    // put API Url's etc. here; everything, that's constant
    C: (function() {
      var constants = {
        UrlJsonLocation: 'getLatLongForAddress.php?address=__ADDRESS__',
          // to statically retrieve location information based on an address or location
        UrlUserInfo: 'http://ws.audioscrobbler.com/2.0/?method=user.getinfo&user=__USER_NAME__&api_key=__API_KEY__',
          // can be personalized with .replace() later
        UrlPastEvents: 'http://ws.audioscrobbler.com/2.0/?method=user.getpastevents&user=__USER_NAME__&api_key=__API_KEY__&limit=__LIMIT__',
          // for past events
        UrlAttendees: 'http://ws.audioscrobbler.com/2.0/?method=event.getattendees&event=__EVENT_ID__&api_key=__API_KEY__&limit=__LIMIT__',
          // Url for attendees of an events
        UrlFutureEvents: 'http://ws.audioscrobbler.com/2.0/?method=user.getevents&user=__USER_NAME__&api_key=__API_KEY__&limit=__LIMIT__'
          // Url for future events
      };
      
      return typeof Object.freeze === 'function' ? Object.freeze(constants) : constants;
        // try to freeze here, as const is not cross-browser
    }()),

    
    /**
     * Should be called once to retrieve the token (API-key)
     */
    retrieveToken: function() {
      var $t = this;
      
      if ($t.token !== null) {
        return;
      }
      
      $.ajax({
        data: null,
        dataType: 'json',
        type: 'get',
        url: 'res/token.js'
      }).done(function(json) {
        $t.token = json.token;
      });
    },
    
    
    /**
     * Retrieves the userdata and transforms it into an object of type LastFmUser.
     *
     * @param String name the user's name (not the real name)
     * @param Function callback a function that must accept one parameter of type
     *  LastFmUser. If any error occurs, it will receive null.
     */
    getUserByName: function(name, callback) {
      var $t = this;
      
      $.ajax({
        data: null,
        dataType: 'xml',
        type: 'get',
        url: $t.C.UrlUserInfo.replace('__USER_NAME__', name).replace('__API_KEY__', $t.token)
      }).done(function(xml) {
        var realName, url, picUrl;
        
        if ($(xml).children('lfm:first-child').attr('status') !== 'ok') {
          callback(null);
          return;
        }
        
        name = $(xml).find('name').text();
        realName = $(xml).find('realname').text();
        url = $(xml).find('url').text();
        picUrl = $(xml).find('image[size="extralarge"]').text();
        
        callback(new LastFmUser(name, realName, url, picUrl));
      }).fail(function() {
        callback(null); // es ist etwas schief gelaufen
      });
    },
    
    
    /**
     * Gets all attendees for one event (up to 1000).
     *
     * @param mixed eventId the LastFmEvent-ID
     * @param Function progressCallback Function that receives the step-wise
     *  (atomic) progress, between 0 and 1. Can be null.
     * @param Function callback, that gets (a may be empty) array of LastFmUsers
     */
    getAttendeesByEventId: function(eventId, progressCallback, callback) {
      var $t = this;
      
      $.ajax({
        data: null,
        dataType: 'xml',
        type: 'get',
        url: $t.C.UrlAttendees.replace('__EVENT_ID__', eventId + '')
          .replace('__API_KEY__', $t.token).replace('__LIMIT__', '1000'),
        xhr: function() {
          var xhr = jQuery.ajaxSettings.xhr();
          
          if (typeof progressCallback === 'function'
              && xhr instanceof window.XMLHttpRequest
              && typeof xhr.addEventListener === 'function') {
            xhr.addEventListener('progress', function(evt) {
              if (evt.lengthComputable) {
                progressCallback(evt.loaded / evt.total);
              }
            }, false);
          }
          
          return xhr;
        }
      }).done(function(xml) {
        var r = [];
        
        $(xml).find('user').each(function(i, e) {
          var name, realName, url, picUrl;
          name = $(e).find('name').text();
          realName = $(e).find('realname').text();
          url = $(e).find('url').text();
          picUrl = $(e).find('image[size="extralarge"]').text();
          r.push(new LastFmUser(name, realName, url, picUrl));
        });
        
        callback(r);
      }).fail(function() {
        callback([]);
      });
    },
    
    
    /**
     * The functions getPastEventsByUserName() and getFutureEventsByUserName() have
     * been merged into this one. This function gets all events for one user.
     *
     * @param Boolean pastEvents true, if past events, false otherwise
     * @param String name the user's name (not the real name)
     * @param int limit the maximum amount of events to fetch
     * @param Function progressCallback function, for atomic progress, can be null
     * @param Function callback that receives an array of LastFmEvents
     */
    getEventsByUserName: function(pastEvents, name, limit, progressCallback, callback) {
      var $t = this;
      
      $.ajax({
        data: null,
        dataType: 'xml',
        type: 'get',
        url: (pastEvents === true ? $t.C.UrlPastEvents : $t.C.UrlFutureEvents)
          .replace('__USER_NAME__', name + '')
          .replace('__API_KEY__', $t.token).replace('__LIMIT__', limit + ''),
        xhr: function() {
          var xhr = jQuery.ajaxSettings.xhr();
          
          if (typeof progressCallback === 'function'
              && xhr instanceof window.XMLHttpRequest
              && typeof xhr.addEventListener === 'function') {
            xhr.addEventListener('progress', function(evt) {
              if (evt.lengthComputable) {
                progressCallback(evt.loaded / evt.total);
              }
            }, false);
          }
          
          return xhr;
        }
      }).done(function(xml) {
        var r = [];

        $(xml).find('event').each(function(i, e) {
          var id, title, venue, locName,
            city, country, geopoint, lat, lon,
            lastfmUrl, website, image, date;
          
          id = $(e).children('id').text();
          title = $(e).children('title').text();
          venue = $(e).children('venue')[0];
          locName = $(venue).children('name').text();
          
          city = $(venue).find('city').text();
          country = $(venue).find('country').text();
          geopoint = $(venue).find('location').children()[4];
          lat = $($(geopoint).children()[0]).text();
          lon = $($(geopoint).children()[1]).text();
          
          lastfmUrl = $(e).children('url').text();
          website = $(e).children('website').text();
          image = $(e).children('image[size="large"]').text();
          date = Date.parse($(e).children('startDate').text()) / 1000;
            // sth. like Thu, 30 Jun 2011 13:00:00 becomes 10-digit UNIX timestamp
          
          r.push(new LastFmEvent(id, title, locName, city, country, lat, lon, lastfmUrl, website, image, date));
        });
        
        callback(r);
      }).fail(function() {
        callback([]);
      });
    },
    
    
    /**
     * Tries to utilize HTML5' geolocation API.
     *
     * @param Function successCallback should accept one parameter and will
     *  be given an object with the keys lat and lon
     * @param Function errorCallback always called if timeout, perm denied,
     *  API not available, when the pos is unavailable or an unknown error
     *  occurred
     * @throws Exception if one of the callbacks is not of type Function
     */
    getUserLocation: function(successCallback, errorCallback) {
      if (typeof successCallback !== 'function' || typeof errorCallback !== 'function') {
        throw 'Both callbacks must not be null and of type Function.';
      }
      
      if (navigator.geolocation && typeof navigator.geolocation.getCurrentPosition === 'function') {
        navigator.geolocation.getCurrentPosition(function(position) {
          successCallback({ lat: position.coords.latitude, lon: position.coords.longitude });
        }, function(error) {
          errorCallback(error);
        }, { enableHighAccuracy: true, timeout: 2500 });
      } else {
        errorCallback();
      }
    },
    
    
    /**
     * Shortcut-Function to intercept and cancel events.
     *
     * @param HTMLDomEvent e the event to intercept
     * @return Boolean false, useful when chaining etc.
     */
    sE: function(e) {
      e.stopPropagation();
      e.preventDefault();
      return false;
    },
    
    
    /**
     * Little helper to mass-wise (de-)activate elements
     *
     * @param Boolean enable true, to enable the elements
     * @param Array elems array of ID's of elements
     */
    enDis: function(enable, elems) {
      var i;
      for (i = 0; i < elems.length; i++) {
        if (enable) {
          $('#' + elems[i]).removeAttr('disabled');
        } else {
          $('#' + elems[i]).attr('disabled', 'disabled');
        }
      }
    },
    
    
    /**
     * Renders the suggested events by filtering them first. Calculates the absolute
     * ranking meanwhile.
     *
     * @param SuggestedEvents suggEvts the object holding the suggested events
     * @param long minDate the lower boundary of the date as Unix-timestamp (seconds only)
     * @param long maxDate the upper boundary for the date
     * @param Object pos object with keys lat and lon
     * @param int maxDistance the maximum distance in km
     */
    renderResults: function(suggEvts, minDate, maxDate, pos, maxDistance) {
      var d, e, i, results = $('section#results'), evts = suggEvts.filterEvents(
          minDate, maxDate, pos.lat, pos.lon, maxDistance).calculateAbsoluteRanking().getEvents(),
        /**
         * Private static shorthand function to create elements quickly
         *
         * @param String e the tagname of the element to create
         * @return HTMLDomElement the element
         */
        ce = function(e) { return document.createElement(e); },
        
        /**
         * Private static function to add a marker to the map and bounds. Also
         * adds the clicklistener, that scrolls to the correct position.
         *
         * @param LastFmEvent e the event
         * @param google.maps.Map map the map to add it to
         * @param google.maps.LatLngBounds the bounds to add it to
         */
        addMarker = function(e, map, bounds) {
          google.maps.event.addListener(new google.maps.Marker({
            position: new google.maps.LatLng(e.getLat(), e.getLon()),
            map: map,
            title: e.getTitle()
          }), 'click', (function(e) {
            return function() {
              $('html').animate({ scrollTop: $('div#evt-' + e.getId()).offset().top + 'px' }, {
                duration: 1000, complete: function() {
                  document.location.hash = 'evt-' + e.getId();
                }});
            };
          }(e)));
          
          bounds.extend(new google.maps.LatLng(e.getLat(), e.getLon()));
          map.fitBounds(bounds);
        },
        
        bounds = new google.maps.LatLngBounds(),
          // bounds and map
        map = new google.maps.Map(document.getElementById('map'), {
          zoom: 7,
          center: new google.maps.LatLng(
            evts.length > 0 ? evts[0].theEvent.getLat() : 0,
            evts.length > 0 ? evts[0].theEvent.getLon() : 0),
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });
      
      results.empty().show(0);
      
      // renders the results
      for (i = 0; i < evts.length; i++) {
        e = evts[i].theEvent;
        d = new Date(e.getDate() * 1000);
        
        $(ce('div'))
          .attr('id', 'evt-' + e.getId())
          .addClass('lfm-result')
          .append($(ce('img'))
            .attr('src', e.getImage()))
          .append($(ce('h1'))
            .text(e.getTitle()))
          .append($(ce('p'))
            .text((d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear() + ', '
              + (evts[i].absWeight * 100).toFixed(2) + '% match, '
              + e.getCountry() + ', ' + e.getCity() + ', '
              + SuggestedEvents.calculateDistance(
                pos.lat, pos.lon, e.getLat(), e.getLon()).toFixed(0) + ' km away')
            .append(ce('br'))
            .append($(ce('a'))
              .attr({ target: '_blank', title: 'Go to the page on Last.fm', href: e.getLastfmUrl() })
              .text(decodeURI(e.getLastfmUrl()))))
          .append($(ce('span'))
            .addClass('map-icon')
            .bind('click', (function(e) {
              return function() {
                $('html').animate({ scrollTop: $('div#map').offset().top + 'px' }, {
                  duration: 1000, complete: function() {
                    map.setCenter(new google.maps.LatLng(e.getLat(), e.getLon()));
                    document.location.hash = 'map';
                  }});
              };
            }(e))))
          .append($(ce('div')).addClass('clearleft'))
          .appendTo(results);
        
        // no valid coords were supplied, we try to retrieve some
        if (e.getLat() === '' || e.getLon() === '') {
          with({ _e: e }) {
            $.ajax({
              data: null,
              dataType: 'json',
              type: 'get',
              url: twoME107.C.UrlJsonLocation.replace('__ADDRESS__', _e.getCity())
            }).done(function(json) {
              if (!json.error) {
                _e.setLat(json.lat);
                _e.setLon(json.lon);
                addMarker(_e, map, bounds);
              }
            });
          }
        } else {
          addMarker(e, map, bounds);
        }
      }
      
      if (i === 0) {
        alert('There are no results to display. Try to adjust the weight of'
          + '\nthe distance or to change the maximum distance to a higher'
          + '\n value or even unlimited.');
      }
      
      $('html').animate({ scrollTop: $('div#map').offset().top + 'px' }, { duration: 1000 });
      map.fitBounds(bounds);
    }
  };

  
  // twoME107-Object exposen und initialisieren
  window.twoME107 = typeof Object.seal === 'function' ? Object.seal(twoME107) : twoME107;
  window.twoME107.retrieveToken(); // API-Key abholen

  
  /**
   * Fires when the DOM is ready. Hooks everything up (listeners etc.)
   */
  $(function() {
    var ud = $('div#userdetails'), uForm = $('form#form-logon'), name = $('input#name'),
      controlSection = $('section#controls'), controlsForm = $('form#form-controls'),
      distanceImp = $('select#distance-importance'), fetchResults = $('input#fetch-results'),
      progWrapper = $('div#progress-wrapper'), progress = $('progress#fetch-progress'),
      progStatus = $('span#progress-status-text'), percent = $('span#progress-status-percent'),
      slider = $('div#radius-slider'),
      minDate = $('input#minimum-date'), maxDate = $('input#maximum-date'),
        // needed in multiple functions
      
      /**
       * Creates a ranking of the last result, adds absolute weights and initiates the
       * rendering process with the min- and max-date, distance and distance weight.
       *
       * @see twoME107.renderResults()
       */
      reRender = function() {
        var pos = JSON.parse($.cookie('position') || '{"lat":0,"lon":0}'),
          distImp = distanceImp.val();
        
        if (twoME107.lastResult === null) {
          return;
        }
        
        twoME107.renderResults(
          twoME107.lastResult.createRanking(pos.lat, pos.lon, parseFloat(distImp)),
            Date.parse(minDate.val()) / 1000, Date.parse(maxDate.val()) / 1000,
            pos, slider.slider('value') > 1000 || distImp === '0' ? 10e6 : slider.slider('value'));
      };
    
    
    if ($.cookie('position') === null) {
      twoME107.getUserLocation(function(pos) {
        $.cookie('position', JSON.stringify(pos), { expires: 1 /* one day */ });
      }, function() {
        var askForLocation = function(loc) {
          $.ajax({
            data: null,
            dataType: 'json',
            type: 'get',
            url: twoME107.C.UrlJsonLocation.replace('__ADDRESS__', loc)
          }).done(function(json) {
            if (json.error) {
              if (confirm('Could not fetch coordinates for the last location.'
                  + '\nDo you want to enter another location?')) {
                askForLocation(prompt('Please enter an address or location:', ''));
              }
            } else {
              $.cookie('position', JSON.stringify({ lat: json.lat, lon: json.lon }), { expires: 1 });
              alert('Your position has been stored.');
            }
          });
        };
        // ask user for static location
        if (confirm('The location of your computer could not be retrieved.'
            + '\nDo you want to enter a location instead?')) {
          askForLocation(prompt('Please enter an address or location:', ''));
        }
      });
    }
    
    
    /**
     * When the user 'logs' in
     */
    uForm.bind('submit', function(e) {
      var $f = $(this), elems = ['name', 'submit'];
      
      if (name.val() === '') {
        name.focus();
        return twoME107.sE(e);
      }
      
      twoME107.enDis(false, elems);

      twoME107.getUserByName(name.val(), function(lastFmUser) {
        twoME107.enDis(true, elems);

        if (lastFmUser === null) {
          name.focus();
        } else {
          $f.hide(500, function() {
            $('img#avatar').bind('load', function() {
              $(this).css('opacity', 0).animate({ opacity: 1 }, { duration: 750 });
            }).attr('src', lastFmUser.getPicUrl() === '' ?
              'res/anonymous.png' : lastFmUser.getPicUrl());
            $('h1#username').text(lastFmUser.getRealName() === '' ? lastFmUser.getName() :
              (lastFmUser.getRealName() + ' (' + lastFmUser.getName() + ')'));
            $('a#userlink').attr('href', lastFmUser.getUrl()).text(lastFmUser.getUrl());
            
            ud.show(500);
            progWrapper.hide(0);
            twoME107.enDis(true, ['fetch-results']);
            controlSection.show(500);
          });
        }
      });
      
      return twoME107.sE(e); // Event was consumed
    });


    /**
     * This handler fires when the user hits the get results button. This
     * is what happens:
     * - get the last 25 events
     * - get all attendees of those events
     * - add them to an object of type SimilarUsers
     *  -> this should result in a ranking; the more similar events, the
     *     higher the ranking of the attendee
     * - get the future events of the top 25 attendees
     * - add them via addEvent() in object of type SuggestedEvents with
     *   the attendee's ranking; results in a total ranking regarding the
     *   user and future events
     */
    controlsForm.bind('submit', function(e) {
      var lowerDate, upperDate, radius = slider.slider('value'),
        numPastEvents = 25, numFutureEvents = 25, numAttendees = 25, i, reqsReady = 0,
        simUsers = new SimilarUsers(name.val());
      
      if (isNaN(lowerDate = Date.parse(minDate.val()))) {
        minDate.focus();
        return twoME107.sE(e);
      } else if (isNaN(upperDate = Date.parse(maxDate.val()))) {
        maxDate.focus();
        return twoME107.sE(e);
      }
      
      twoME107.enDis(false, ['fetch-results']);
      progWrapper.show(0);
      fetchResults.hide(0);
      progress.val(0);
      progStatus.text('Fetching the last ' + numPastEvents + ' events you visited..');
      
      /*
       * Get the past events of the 'logged-in' user
       */
      twoME107.getEventsByUserName(true, name.val(), numPastEvents, null, function(events) {
        var j, atomicProgress = 1 / (numAttendees + (numPastEvents = events.length) + 1);
        progress.val(atomicProgress);
        percent.text((atomicProgress * 100).toFixed(0) + '%');
        
        if (numPastEvents === 0) {
          progress.val(1);
          percent.text('100%');
          progStatus.text('You haven\'t visited any events in the past.');
          return;
        }
        
        /*
         * Fetch all users of the past events here. Starts a number of ajax
         * calls. If the last one returns, the list is being sorted.
         */
        for (j = 0; j < numPastEvents; j++) {
          window.setTimeout((function(evtId) {
            var numAttendees = 10;
            return function() {
              twoME107.getAttendeesByEventId(evtId, null, function(users) {
                var k, m, sliceUsers = [], suggEvents = new SuggestedEvents(),
                  atomicProgress = (++reqsReady + 1) / (numAttendees + numPastEvents + 1);

                progress.val(atomicProgress);
                percent.text((atomicProgress * 100).toFixed(0) + '%');
                progStatus.text('Fetching attendees for event ' + reqsReady
                  + ' of ' + numPastEvents);
                
                for (k = 0; k < users.length; k++) {
                  simUsers.addUser(users[k].getName());
                }
                
                simUsers.sortList();
                if (reqsReady === numPastEvents) {
                  if (simUsers.getUsers().length < numAttendees) {
                    numAttendees = simUsers.getUsers().length;
                  }

                  sliceUsers = simUsers.getUsers().slice(0, numAttendees);
                  
                  /*
                   * All users have been fetched, now get all events in the future.
                   * If the last request arrives, store the last result in our helper
                   * at twoME107.lastResult and call the renderer.
                   */
                  for (m = 0; m < sliceUsers.length; m++) {
                    window.setTimeout((function(uName) {
                      return function() {
                        twoME107.getEventsByUserName(false, uName, numFutureEvents, null, function(events) {
                          var n, pos = JSON.parse($.cookie('position')), ranked,
                            atomicProgress = (++reqsReady + 1) / (sliceUsers.length + numPastEvents + 1);
                          
                          progress.val(atomicProgress);
                          percent.text((atomicProgress * 100).toFixed(0) + '%');
                          progStatus.text('Fetching future events for user ' + uName);
                          
                          for (n = 0; n < events.length; n++) {
                            suggEvents.addEvent(events[n], simUsers.getCommonByUserName(uName));
                          }
                          
                          if (reqsReady === numPastEvents + sliceUsers.length) {
                            progStatus.text('');
                            twoME107.lastResult = suggEvents;
                            reRender();
                          }
                        });
                      };
                    }(sliceUsers[m].name)), m * 50);
                  }
                }
              });
            };
          }(events[j].getId())), j * 50); // to prevent DDoS detection
        }
      });
      
      
      return twoME107.sE(e);
    });


    // Slider initialize
    slider.slider({
      min: 10,
      max: 1010,
      step: 10,
      value: 750,
      slide: function(evt, ui) {
        $('#radius-slider-value').val(ui.value === 1010 ? 'unlimited' : ui.value + ' km');
      },
      change: function(evt, ui) {
        reRender();
      }
    });
    $('#radius-slider-value').val(slider.slider('value') + ' km');
    
    // enable datepicker
    minDate.bind('change', function() { reRender(); }).datepicker();
    maxDate.bind('change', function() { reRender(); }).datepicker();
    
    distanceImp.bind('change', function() { reRender(); });
    
    
    /**
     * If another user shall be used.
     */
    $('a#switch-user').bind('click', function() {
      name.val('');
      progWrapper.hide(0);
      fetchResults.show(0);
      controlSection.hide(0);
      ud.hide(500, function() {
        uForm.show(500);
      });
    });
  });
}());